<?php
/**
 * Note: 网页授权获得auth_code, 然后获取accessToken, 查询子账户ID, 后续根据子账户ID查询数据
 */

namespace App\Services\Promo;

use Carbon\Carbon;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class BaiduService
{
    protected const API_ACCESS_TOKEN = 'https://u.baidu.com/oauth/accessToken';
    protected const API_REFRESH_TOKEN = 'https://u.baidu.com/oauth/refreshToken';
    protected const API_ACCOUNT_INFO = 'https://u.baidu.com/oauth/getUserInfo';
    protected const API_CUSTOM_REPORT = 'https://api.baidu.com/json/sms/service/OpenApiReportService/getReportData';

    protected static array $configs = [];

    public const KEY_ACCESS_TOKEN = 'accessToken';
    public const KEY_REFRESH_TOKEN = 'refreshToken';
    public const KEY_USERID = 'userId';
    public const KEY_OPENID = 'openId';
    public const KEY_USERS = 'users';
    public const KEY_ADVERTISERS = 'advertisers';

    /**
     * @param null $key
     * @return array|\Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed|null
     */
    protected function getConfig($key = null)
    {
        if (!self::$configs) self::$configs = config('promo.baidu');

        return $key ? (self::$configs[$key] ?? null) : self::$configs;
    }

    public function get(string $key, $default = null)
    {
        // 优先获取新缓存, 如果未获取到, 就尝试获取旧缓存
        $key = __CLASS__ . $key;
        $data = Cache::get($key, $default);
        if (isset($data)) return $data;

        // 兼容旧缓存
        return Cache::get(str_replace('\Promo', '', $key), $default);
    }

    public function set(string $key, $val, $ttl = null)
    {
        $key = __CLASS__ . $key;
        Cache::put($key, $val, $ttl);
    }

    /**
     * 发送get请求
     * @param $url
     * @param $data
     * @return false|PromiseInterface|Response
     * @throws \Exception
     */
    protected function requestGet($url, $data)
    {
        // 发生错误时 100ms 后重试 1 次, 每次超时3s (设置timeout之后, 如果超时会直接抛出异常, 不到 $response->throw), 这里放入try
        try {
            info(__METHOD__, [$url, $data]);
            $response = Http::retry(2, 100)->timeout(3)->asForm()->get($url, $data);
            $response->throw();
            info(__METHOD__, [$response]);
        } catch (\Exception $e) {
            report($e);
            throw new \Exception("百度正在维护中，请联系客服");
        }
        return $response;
    }

    /**
     * 发送post请求
     * @param string $url
     * @param array $data
     * @return false|PromiseInterface|Response
     * @throws \Exception
     */
    protected function requestPost(string $url, array $data)
    {
        // 发生错误时 100ms 后重试 1 次, 每次超时3s (设置timeout之后, 如果超时会直接抛出异常, 不到 $response->throw), 这里放入try
        try {
            info(__METHOD__, [$url, $data]);
            $response = Http::retry(2, 100)->timeout(3)->asJson()->post($url, $data);
            $response->throw();
            info(__METHOD__, [$response]);
            if (isset($response['code']) && $response['code'] > 0) throw new \Exception($response['message']);
        } catch (\Exception $e) {
            report($e);
            throw new \Exception($e->getMessage());
        }
        return $response;
    }

    /**
     * 获取身份鉴权用的access_token
     * @param string $authCode 在百度开发者后台使用回调接口获得
     * @param int $userId
     * @return string
     * @throws \Exception
     */
    public function getAccessToken(string $authCode = '', int $userId = 0): string
    {
        // 重新授权
        if ($authCode) {
            $param = [
                'appId' => $this->getConfig('app_id'),
                'authCode' => $authCode,
                'secretKey' => $this->getConfig('secret'),
                'grantType' => 'auth_code',
                'userId' => $userId,
            ];
            $response = $this->requestPost(self::API_ACCESS_TOKEN, $param);
            // 缓存
            $this->cacheResponse($response);

            return $response['data'][self::KEY_ACCESS_TOKEN];
        }

        // 使用缓存, 没有缓存则使用refresh_token进行刷新
        $accessToken = $this->get(self::KEY_ACCESS_TOKEN);
        if ($accessToken) return $accessToken;

        return $this->refreshToken();
    }

    /**
     * @return string
     * @throws \Exception
     */
    public function refreshToken(): string
    {
        if (!$this->get(self::KEY_REFRESH_TOKEN)) {
            throw new \Exception('刷新token失败, 本地无token缓存, 请先获取授权');
        }
        $param = [
            'appId' => $this->getConfig('app_id'),
            'refreshToken' => $this->get(self::KEY_REFRESH_TOKEN),
            'secretKey' => $this->getConfig('secret'),
            'userId' => $this->get(self::KEY_USERID),
        ];
        $response = $this->requestPost(self::API_REFRESH_TOKEN, $param);

        // 缓存
        $this->cacheResponse($response);

        return $response['data'][self::KEY_ACCESS_TOKEN];
    }

    /**
     * 由于此方法只会在刷新accessToken后运行, 刷新时间是一天, 若在此期间新建了广告主ID, 系统会有一天的延迟, 此时可以重新授权, 获得最新的数据
     * @param Response $response
     * @throws \Exception
     */
    private function cacheResponse(Response $response)
    {
        $this->set(self::KEY_ACCESS_TOKEN, $response['data'][self::KEY_ACCESS_TOKEN], $response['data']['expiresIn'] - 30);
        $this->set(self::KEY_REFRESH_TOKEN, $response['data'][self::KEY_REFRESH_TOKEN], $response['data']['refreshExpiresIn'] - 30);
        $this->set(self::KEY_USERID, $response['data'][self::KEY_USERID]);
        $this->set(self::KEY_OPENID, $response['data'][self::KEY_OPENID]);

        // 获取子账户ucname
        $this->refreshUserName();
    }

    /**
     * 获取账户信息并存储纵横账号ID
     * @throws \Exception
     */
    // public function refreshAccountIds()
    // {
    //     $response = $this->requestPost(self::API_ACCOUNT_INFO, [
    //         'openId' => $this->get(self::KEY_OPENID),
    //         'accessToken' => $this->get(self::KEY_ACCESS_TOKEN),
    //         'userId' => $this->get(self::KEY_USERID),
    //         'needSubList' => true,
    //         'pageSize' => 400,
    //         'lastPageMaxUcId' => 1,
    //     ]);
    //     if (!$response) throw new \Exception('获取账户信息失败');
    //     if (empty($response['data']['subUserList'])) throw new \Exception('已授权账户为空');
    //     $this->set(self::KEY_ACCOUNT_IDS, array_column($response['data']['subUserList'], 'ucId'));
    // }

    /**
     * 获取子账户ucname
     * @return void
     * @throws \Exception
     */
    public function refreshUserName()
    {
        $response = $this->requestPost(self::API_ACCOUNT_INFO, [
            'openId' => $this->get(self::KEY_OPENID),
            'accessToken' => $this->get(self::KEY_ACCESS_TOKEN),
            'userId' => $this->get(self::KEY_USERID),
            'needSubList' => true,
            'pageSize' => 400,
            'lastPageMaxUcId' => 1,
        ]);
        if (!$response) throw new \Exception('获取账户信息失败');
        if (empty($response['data']['subUserList'])) throw new \Exception('已授权账户为空');
        $users = [];
        $ads = [];
        foreach ($response['data']['subUserList'] as $user) {
            $users[$user['ucId']] = $user['ucName'];
            $ads[] = [
                'id'=> $user['ucName'],
                'name' => $user['ucName'],
            ];
        }
        $this->set(self::KEY_ADVERTISERS, $ads);
        $this->set(self::KEY_USERS, $users);
    }

    /**
     * 推广报告 - 账户报告
     * @param string $userName
     * @param array $timeRange
     * @return int
     * @throws \Exception
     */
    public function accountCost(string $userName, array $timeRange = []): int
    {
        $timeRange = array_map(function ($v) {
            if ($v instanceof Carbon) return $v->format('Y-m-d');
            return $v;
        }, $timeRange);

        $param = [
            'header' => [
                'userName' => $userName,
                'accessToken' => $this->get(self::KEY_ACCESS_TOKEN),
            ],
            'body' => [
                "reportType" => 1783967,
                "timeUnit" => "DAY",
                "startDate" => $timeRange[0],
                "endDate" => $timeRange[1],
                "columns" => [
                    "date",
                    "cost",
                    "userName"
                ],
                "needSum" => true,
            ],
        ];
        $this->getAccessToken();
        $response = $this->requestPost(self::API_CUSTOM_REPORT, $param, []);
        if (!$response) return 0;
        return $response['body']['data'][0]['summary']['cost'] ?? 0;
        // 花费 body.data.summary.cost
    }
}
